package com.comment.util.ip;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

public class Searcher {
    public static final int HeaderInfoLength = 256;
    public static final int VectorIndexRows = 256;
    public static final int VectorIndexCols = 256;
    public static final int VectorIndexSize = 8;
    public static final int SegmentIndexSize = 14;
    private final RandomAccessFile handle;
    private int ioCount = 0;
    private final byte[] vectorIndex;
    private final byte[] contentBuff;
    public static final byte[] shiftIndex = new byte[]{24, 16, 8, 0};

    public static Searcher newWithFileOnly(String dbPath) throws IOException {
        return new Searcher(dbPath, (byte[]) null, (byte[]) null);
    }

    public static Searcher newWithVectorIndex(String dbPath, byte[] vectorIndex) throws IOException {
        return new Searcher(dbPath, vectorIndex, (byte[]) null);
    }
    public static Searcher newWithVectorIndex(File file , byte[] vectorIndex) throws IOException {
        return new Searcher(file, vectorIndex);
    }

    public static Searcher newWithBuffer(byte[] cBuff) throws IOException {
        return new Searcher((String) null, (byte[]) null, cBuff);
    }

    public Searcher(File file, byte[] vectorIndex) throws IOException {
        this.handle = new RandomAccessFile(file, "r");
        this.vectorIndex = vectorIndex;
        this.contentBuff = null;
    }

    public Searcher(String dbFile, byte[] vectorIndex, byte[] cBuff) throws IOException {
        if (cBuff != null) {
            this.handle = null;
            this.vectorIndex = null;
            this.contentBuff = cBuff;
        } else {
            this.handle = new RandomAccessFile(dbFile, "r");
            this.vectorIndex = vectorIndex;
            this.contentBuff = null;
        }

    }


    public void close() throws IOException {
        if (this.handle != null) {
            this.handle.close();
        }

    }

    public int getIOCount() {
        return this.ioCount;
    }

    public String search(String ipStr) throws Exception {
        long ip = checkIP(ipStr);
        return this.search(ip);
    }

    public String search(long ip) throws IOException {
        this.ioCount = 0;
        int sPtr = 0;
        int ePtr = 0;
        int il0 = (int) (ip >> 24 & 255L);
        int il1 = (int) (ip >> 16 & 255L);
        int idx = il0 * 256 * 8 + il1 * 8;
        byte[] buff;
        if (this.vectorIndex != null) {
            sPtr = getInt(this.vectorIndex, idx);
            ePtr = getInt(this.vectorIndex, idx + 4);
        } else if (this.contentBuff != null) {
            sPtr = getInt(this.contentBuff, 256 + idx);
            ePtr = getInt(this.contentBuff, 256 + idx + 4);
        } else {
            buff = new byte[8];
            this.read(256 + idx, buff);
            sPtr = getInt(buff, 0);
            ePtr = getInt(buff, 4);
        }

        buff = new byte[14];
        int dataLen = -1;
        int dataPtr = -1;
        int l = 0;
        int h = (ePtr - sPtr) / 14;

        while (l <= h) {
            int m = l + h >> 1;
            int p = sPtr + m * 14;
            this.read(p, buff);
            long sip = getIntLong(buff, 0);
            if (ip < sip) {
                h = m - 1;
            } else {
                long eip = getIntLong(buff, 4);
                if (ip <= eip) {
                    dataLen = getInt2(buff, 8);
                    dataPtr = getInt(buff, 10);
                    break;
                }

                l = m + 1;
            }
        }

        if (dataPtr < 0) {
            return null;
        } else {
            byte[] regionBuff = new byte[dataLen];
            this.read(dataPtr, regionBuff);
            return new String(regionBuff, "utf-8");
        }
    }

    protected void read(int offset, byte[] buffer) throws IOException {
        if (this.contentBuff != null) {
            System.arraycopy(this.contentBuff, offset, buffer, 0, buffer.length);
        } else {
            assert this.handle != null;

            this.handle.seek((long) offset);
            ++this.ioCount;
            int rLen = this.handle.read(buffer);
            if (rLen != buffer.length) {
                throw new IOException("incomplete read: read bytes should be " + buffer.length);
            }
        }
    }

    public static Header loadHeader(RandomAccessFile handle) throws IOException {
        handle.seek(0L);
        byte[] buff = new byte[256];
        handle.read(buff);
        return new Header(buff);
    }

    public static Header loadHeaderFromFile(String dbPath) throws IOException {
        RandomAccessFile handle = new RandomAccessFile(dbPath, "r");
        Header header = loadHeader(handle);
        handle.close();
        return header;
    }

    public static byte[] loadVectorIndex(RandomAccessFile handle) throws IOException {
        handle.seek(256L);
        int len = 917504;
        byte[] buff = new byte[len];
        int rLen = handle.read(buff);
        if (rLen != len) {
            throw new IOException("incomplete read: read bytes should be " + len);
        } else {
            return buff;
        }
    }

    public static byte[] loadVectorIndexFromFile(String dbPath) throws IOException {
        RandomAccessFile handle = new RandomAccessFile(dbPath, "r");
        byte[] vIndex = loadVectorIndex(handle);
        handle.close();
        return vIndex;
    }

    public static byte[] loadVectorIndexFromFile(File file) throws IOException {
        RandomAccessFile handle = new RandomAccessFile(file, "r");
        byte[] vIndex = loadVectorIndex(handle);
        handle.close();
        return vIndex;
    }

    public static byte[] loadContent(RandomAccessFile handle) throws IOException {
        handle.seek(0L);
        byte[] buff = new byte[(int) handle.length()];
        int rLen = handle.read(buff);
        if (rLen != buff.length) {
            throw new IOException("incomplete read: read bytes should be " + buff.length);
        } else {
            return buff;
        }
    }

    public static byte[] loadContentFromFile(String dbPath) throws IOException {
        RandomAccessFile handle = new RandomAccessFile(dbPath, "r");
        byte[] content = loadContent(handle);
        handle.close();
        return content;
    }

    public static long getIntLong(byte[] b, int offset) {
        return (long) b[offset++] & 255L | (long) (b[offset++] << 8) & 65280L | (long) (b[offset++] << 16) & 16711680L | (long) (b[offset] << 24) & 4278190080L;
    }

    public static int getInt(byte[] b, int offset) {
        return b[offset++] & 255 | b[offset++] << 8 & '\uff00' | b[offset++] << 16 & 16711680 | b[offset] << 24 & -16777216;
    }

    public static int getInt2(byte[] b, int offset) {
        return b[offset++] & 255 | b[offset] & '\uff00';
    }

    public static String long2ip(long ip) {
        return String.valueOf(ip >> 24 & 255L) + '.' + (ip >> 16 & 255L) + '.' + (ip >> 8 & 255L) + '.' + (ip & 255L);
    }

    public static long checkIP(String ip) throws Exception {
        String[] ps = ip.split("\\.");
        if (ps.length != 4) {
            throw new Exception("invalid ip address `" + ip + "`");
        } else {
            long ipDst = 0L;

            for (int i = 0; i < ps.length; ++i) {
                int val = Integer.parseInt(ps[i]);
                if (val > 255) {
                    throw new Exception("ip part `" + ps[i] + "` should be less then 256");
                }

                ipDst |= (long) val << shiftIndex[i];
            }

            return ipDst & 4294967295L;
        }
    }
}